﻿using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Text;
using System.Threading;
using SG.Sys.Console.CommandProcessing;

namespace SG.Sys.Console.Process
{
    public abstract class ProcessWrapperBase : IProcessWrapper
    {
        public event EventHandler<LineOutputEvent> OnOutput;

        protected ProcessWrapperBase(ProcessStartInfo info, char promptChar = '>')
            : this(info, promptChar, Encoding.UTF8, Encoding.UTF8)
        {
            // nothing to do            
        }

        protected ProcessWrapperBase(ProcessStartInfo info, char promptChar, Encoding output = null, Encoding error = null)
        {
            StartInfo = info;

            StartInfo.RedirectStandardError = true;
            StartInfo.RedirectStandardInput = true;
            StartInfo.RedirectStandardOutput = true;
            StartInfo.UseShellExecute = false;
            if (output != null)
                StartInfo.StandardOutputEncoding = output;
            if (error != null)
                StartInfo.StandardErrorEncoding = error;

            PromptChar = promptChar;
            Title = Name;
        }

        public void Start()
        {
            Process = System.Diagnostics.Process.Start(StartInfo);

            foreach (var context in Enum.GetValues(typeof(OutputKind)))
                new Thread(ReadStream).Start(context);            
        }

        void ReadStream(object threadParam)
        {
            var context = (OutputKind) threadParam;

            switch (context)
            {
                case OutputKind.Error:
                    ReadErrorStream(Process.StandardError, context);
                    return;
                case OutputKind.Output:
                    ReadOutputStream(Process.StandardOutput, context);
                    return;
                default:
                    throw new ArgumentException(string.Format("Context {0} is not known!", context), "threadParam");
            }
        }

        void ReadOutputStream(StreamReader reader, OutputKind context)
        {
            var lines = new List<StringBuilder>();
            var currentBuffer = 0;

            var lookForPrompt = true;
            var seenPrompt = false;
            int read;

            while ((read = reader.Read()) >= 0)
            {
                var flush = false;
                var c = (char)read;

                if (currentBuffer == lines.Count)
                    lines.Add(new StringBuilder(82));
                if (c == '\n')
                {
                    lines[currentBuffer++].Append(Environment.NewLine);
                    seenPrompt = false;
                    lookForPrompt = true;
                    flush = true;
                }
                else if (c != '\r')
                    lines[currentBuffer].Append(c);

                // TODO make more generic (opposite char function)
                if (PromptChar == '>' && c == '<')
                    lookForPrompt = false;

                if (lookForPrompt && c == PromptChar)
                {
                    seenPrompt = true;
                    lookForPrompt = false;
                    flush = true;
                }

                var copy = OnOutput;
                if (copy != null && flush)
                {
                    for (var i = 0; i < currentBuffer + (seenPrompt ? 1 : 0); i++)
                    {
                        var buffer = lines[i];
                        copy(this, new LineOutputEvent(buffer.ToString(), context, seenPrompt, seenPrompt ? 0 : 1));

                        buffer.Clear();
                    }
                    currentBuffer = 0;
                }
            }
        }

        void ReadErrorStream(StreamReader reader, OutputKind context)
        {
            var currentBuffer = new StringBuilder(200);
            int lineCount = 0;
            int read;
            bool flush = false;

            while ((read = reader.Read()) >= 0)
            {
                var c = (char)read;

                if (c == '\n')
                {
                    currentBuffer.Append(Environment.NewLine);
                    lineCount++;
                    flush = true;
                }
                else if (c != '\r')
                    currentBuffer.Append(c);

                var copy = OnOutput;
                if (flush && reader.Peek() < 0 && copy != null)
                {
                    copy(this, new LineOutputEvent(currentBuffer.ToString(), context, false, lineCount));
                    currentBuffer.Clear();
                    lineCount = 0;

                    flush = false;
                }
            }
        }

        public ProcessStartInfo StartInfo { get; private set; }
        public System.Diagnostics.Process Process { get; private set; }
        public char PromptChar { get; private set; }
        public string Title { get; protected set; }

        private static readonly ConcurrentDictionary<string, int> IDTable = new ConcurrentDictionary<string, int>();
        private int _id;
        public int ID
        {
            get
            {
                if (_id > 0)
                    return _id;

                var typeName = GetType().AssemblyQualifiedName ?? string.Empty;
                _id = IDTable.AddOrUpdate(typeName, 1, (_, oldID) => oldID + 1);

                return _id;
            }
        }

        public string Name
        {
            get { return string.Format("[{0:d}] {1} {2}", ID, StartInfo.FileName, StartInfo.Arguments); }
        }

        public abstract void SendClose();

        public abstract StateChange SendCommand(Entry entry);
        public abstract Command ParseCommand(string cmdLine);
    }
}
